﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Data.Entity;
using System.Data.Entity.Validation;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using AspAuthentication.Core;
using AspAuthentication.Exceptions;
using AspAuthentication.Persistence.DatabaseScripts;

namespace AspAuthentication.Persistence
{
    /// <summary>
    /// Defines the class UnitOfWork
    /// </summary>
    public class UnitOfWork : IUnitOfWork
    {
        private string ConnectionString { get; set; }

        /// <summary>
        /// Initializes a new instance of the <see cref="UnitOfWork"/> class.
        /// </summary>
        /// <param name="connectionStringName">Name of the connection string.</param>
        /// <param name="createDatabaseObjects">if set to <c>true</c> [create database objects].</param>
        public UnitOfWork(string connectionStringName, bool createDatabaseObjects)
        {
            if (createDatabaseObjects)
            {
                CreateDatabaseIfNotExist(connectionStringName);
                var connectionString = ConfigurationManager.ConnectionStrings[connectionStringName].ConnectionString;
                var tableNames = new List<string> { "DomainUsers", "DomainRoles", "Profiles", "ProfileItems", "DomainRoleDomainUsers" };

                TableCreator.CreateTablesIfNotExist(connectionString, tableNames);

                TableCreator.CreateStoredProcedure(connectionString, new List<string> { "InsertDomainRoleDomainUsers", "DeleteDomainRoleDomainUsers", "DeleteProfiles" });
            }
            
            ConnectionString = connectionStringName;
        }

        /// <summary>
        /// Creates the specified base object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="baseObject">The base object.</param>
        /// <returns></returns>
        public T Create<T>(T baseObject) where T : class
        {
            using (var context = new DomainContext(ConnectionString))
            {
                AddObject(baseObject, context);
                ApplyChangesToDatabase(context);
            }
            return baseObject;
        }

        /// <summary>
        /// Creates the specified base objects.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="baseObjects">The base objects.</param>
        /// <returns></returns>
        public IList<T> Create<T>(IList<T> baseObjects) where T : class
        {
            using (var context = new DomainContext(ConnectionString))
            {
                foreach (var baseObject in baseObjects)
                {
                    AddObject(baseObject, context);
                }
                ApplyChangesToDatabase(context);
            }
            return baseObjects;
        }

        /// <summary>
        /// Updates the specified domain base object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="domainBaseObject">The domain base object.</param>
        /// <returns></returns>
        public T Update<T>(T domainBaseObject) where T : class
        {
            using (var context = new DomainContext(ConnectionString))
            {
                UpdateObject(domainBaseObject, context);
                ApplyChangesToDatabase(context);
            }
            return domainBaseObject;
        }

        /// <summary>
        /// Updates the specified domain base objects.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="domainBaseObjects">The domain base objects.</param>
        /// <returns></returns>
        public IList<T> Update<T>(IList<T> domainBaseObjects) where T : class
        {
            using (var context = new DomainContext(ConnectionString))
            {
                foreach (var domainBaseObject in domainBaseObjects)
                {
                    UpdateObject(domainBaseObject, context);
                }
                ApplyChangesToDatabase(context);
            }
            return domainBaseObjects;
        }

        /// <summary>
        /// Retrieves the domain object.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <returns></returns>
        public T RetrieveDomainObject<T>(Expression<Func<T, bool>> predicate = null) where T : class
        {
            try
            {
                return Search(predicate).First();
            }
            catch (InvalidOperationException exception)
            {
                //TODO:We must log this exception
                if (exception.Message.Equals("Sequence contains no elements"))
                {
                    throw new EntityNotFoundException(string.Format("Could not find {0} with supplied criteria", typeof(T).Name), exception);
                }
                throw;
            }
        }

        /// <summary>
        /// Deletes the domain user.
        /// </summary>
        /// <param name="username">The username.</param>
        public void DeleteDomainUser(string username)
        {
            using (var context = new DomainContext(ConnectionString))
            {
                var user = context.MembershipUsers.Where(u => u.UserName == username).First();
                context.MembershipUsers.Remove(user);
                ApplyChangesToDatabase(context);
            }
        }

        /// <summary>
        /// Deletes the domain role.
        /// </summary>
        /// <param name="rolename">The rolename.</param>
        public void DeleteDomainRole(string rolename)
        {
            using (var context = new DomainContext(ConnectionString))
            {
                var role = context.DomainRoles.Where(r => r.RoleName == rolename).First();
                context.DomainRoles.Remove(role);
                ApplyChangesToDatabase(context);
            }
        }

        /// <summary>
        /// Deletes the profiles.
        /// </summary>
        /// <param name="userNames">The user names.</param>
        public void DeleteProfiles(string[] userNames)
        {
            using (var context = new DomainContext(ConnectionString))
            {
                var profiles = context.Profiles.Where(p => userNames.Contains(p.Username));
                foreach (var profile in profiles)
                {
                    context.Profiles.Remove(profile);
                }
                ApplyChangesToDatabase(context);
            }
        }

        /// <summary>
        /// Searches the specified predicate.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="predicate">The predicate.</param>
        /// <returns></returns>
        public IList<T> Search<T>(Expression<Func<T, bool>> predicate = null) where T : class
        {
            using (var context = new DomainContext(ConnectionString))
            {
                if (typeof(T).Equals(typeof(DomainUser)))
                {
                    Expression<Func<DomainUser, bool>> criteria = u => u.UserName != "";
                    if (predicate != null)
                    {
                        criteria = Convert<T, DomainUser, bool>(predicate);
                    }
                    return (IList<T>)context.MembershipUsers.Include("DomainRoles").Where(criteria).ToList();
                }

                if (typeof(T).Equals(typeof(DomainRole)))
                {
                    Expression<Func<DomainRole, bool>> criteria = c => c.RoleName != "";
                    if (predicate != null)
                    {
                        criteria = Convert<T, DomainRole, bool>(predicate);
                    }
                    return (IList<T>)context.DomainRoles.Where(criteria).ToList();
                }

                if (typeof(T).Equals(typeof(Profile)))
                {
                    Expression<Func<Profile, bool>> criteria = c => c.Username != "";
                    if (predicate != null)
                    {
                        criteria = Convert<T, Profile, bool>(predicate);
                    }
                    return (IList<T>)context.Profiles.Include("ProfileData").Where(criteria).ToList();
                }
            }
            throw new Exception(string.Format("The supplied type {0} is not supported", typeof(T).FullName));
        }

        /// <summary>
        /// Executes the SQL.
        /// </summary>
        /// <param name="sqlCommand">The SQL command.</param>
        public void ExecuteSql(string sqlCommand)
        {
            using (var context = new DomainContext(ConnectionString))
            {
                context.Database.ExecuteSqlCommand(sqlCommand);
            }
        }

        /// <summary>
        /// Retrieves the entities from database.
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="sqlCommand">The SQL command.</param>
        /// <returns></returns>
        public IList<T> RetrieveEntitiesFromDatabase<T>(string sqlCommand) where T : class
        {
            var result = new List<T>();
            using (var context = new DomainContext(ConnectionString))
            {
                result.AddRange(context.Database.SqlQuery<T>(sqlCommand));
            }

            return result;
        }

        private static void UpdateObject<T>(T domainBaseObject, DomainContext context)
        {
            if (domainBaseObject.GetType().Equals(typeof(DomainUser)))
            {
                var user = domainBaseObject as DomainUser;
                context.MembershipUsers.Attach(user);
                context.Entry(user).State = EntityState.Modified;
            }
            else if (domainBaseObject.GetType().Equals(typeof(DomainRole)))
            {
                var role = domainBaseObject as DomainRole;
                context.DomainRoles.Attach(role);
                context.Entry(role).State = EntityState.Modified;
            }
            else if (domainBaseObject.GetType().Equals(typeof(Profile)))
            {
                var profile = domainBaseObject as Profile;
                context.Profiles.Attach(profile);
                context.Entry(profile).State = EntityState.Modified;
            }
            else
            {
                throw new Exception(string.Format("The supplied type {0} is not supported",
                                                  domainBaseObject.GetType().FullName));
            }
        }

        private static void AddObject<T>(T domainBaseObject, DomainContext context)
        {
            if (domainBaseObject.GetType().Equals(typeof(DomainUser)))
            {
                context.MembershipUsers.Add(domainBaseObject as DomainUser);
            }
            else if (domainBaseObject.GetType().Equals(typeof(DomainRole)))
            {
                context.DomainRoles.Add(domainBaseObject as DomainRole);
            }
            else if (domainBaseObject.GetType().Equals(typeof(Profile)))
            {
                var profile = domainBaseObject as Profile;
                var username = profile.Username;
                var retrievedProfile = context.Profiles.Include("ProfileData").Where(p => p.Username == username).ToList().FirstOrDefault();
                if (retrievedProfile != null)
                {
                    context.Profiles.Remove(retrievedProfile);
                }
                context.Profiles.Add(domainBaseObject as Profile);
            }
            else
            {
                throw new Exception(string.Format("The supplied type {0} is not supported",
                                                  domainBaseObject.GetType().FullName));
            }
        }

        private static Expression<Func<TOutput, TResult>> Convert<TInput, TOutput, TResult>(Expression<Func<TInput, TResult>> expression)
        {
            // Add the boxing operation, but get a weakly typed expression
            var converted = Expression.Convert(expression.Body, typeof(TResult));
            // Use Expression.Lambda to get back to strong typing
            return Expression.Lambda<Func<TOutput, TResult>>(converted, expression.Parameters);
        }

        private static void ApplyChangesToDatabase(DbContext context)
        {
            try
            {
                context.SaveChanges();
            }
            catch (DbEntityValidationException ex)
            {
                var stringBuilder = new StringBuilder();
                foreach (var failure in ex.EntityValidationErrors)
                {
                    stringBuilder.AppendFormat("{0} failed validation\n", failure.Entry.Entity.GetType());

                    foreach (var error in failure.ValidationErrors)
                    {
                        stringBuilder.AppendFormat("- {0} : {1}\n", error.PropertyName, error.ErrorMessage);
                    }
                }

                //TODO:Lets log this error to log file.
                throw;
            }
        }

        private static void CreateDatabaseIfNotExist(string connectionString)
        {
            Database.SetInitializer(new CreateDatabaseIfNotExists<DomainContext>());
            using (var context = new DomainContext(connectionString))
            {
                context.Database.CreateIfNotExists();
            }
        }
    }
}


